using System;
using System.Collections;
using System.Reflection;
using System.Text;
using Elf.Core.Assembler;
using Elf.Core.TypeSystem;
using Elf.Core.Runtime.Contexts;
using Elf.Exceptions;
using Elf.Exceptions.Runtime;
using System.Linq;
using Elf.Helpers;

namespace Elf.Core.Runtime.Impl
{
    public class DefaultElfThread : IElfThread
    {
        public VirtualMachine VM { get; private set; }
        public IEntryPoint EntryPoint { get; private set; }
        RuntimeContext IElfThread.RuntimeContext { get { return Ctx; } }
        public ThreadStatus Status { get; private set; }
        public IElfObject ExecutionResult { get; private set; }
        private RuntimeContext Ctx { get; set; }

        public void Bind(VirtualMachine vm)
        {
            VM = vm;
        }

        public void Startup(IEntryPoint entryPoint)
        {
            if (Ctx != null)
            {
                throw new UnexpectedElfRuntimeException(VM, "This thread is already started");
            }
            else
            {
                Ctx = new RuntimeContext();
                Ctx.Bind(VM);
                Ctx.CallStack.Push(new NativeCallContext(entryPoint.CodePoint, entryPoint.This, entryPoint.Args));

                VM.Threads.Add(this);
                EntryPoint = entryPoint;
                ExecutionResult = new ElfVoid();
                Status = ThreadStatus.Running;
            }
        }

        public void Dispose() { /* vm has the responsibility of cleaning up threads that aren't running */ }
        public void Reset() { throw new NotSupportedException(); }
        object IEnumerator.Current { get { return Current; } }

        public ElfVmInstruction Current
        {
            get
            {
                if (Ctx.CallStack.Count == 0)
                {
                    throw new InvalidOperationException();
                }
                else
                {
                    var currentFrame = Ctx.CallStack.Peek();
                    return currentFrame.Source.Body[currentFrame.CurrentEvi];
                }
            }
        }

        public bool MoveNext()
        {
            // todo. implement some protection against infinite loops and stack overflows

            // not very elegant solution, since requires extreme care
            var old = VM.CurrentThread;
            VM.CurrentThread = this;

            try
            {
                if (Ctx.CallStack.Count == 0)
                {
                    return false;
                }
                else
                {
                    var currentFrame = Ctx.CallStack.Peek();
                    if (currentFrame.CurrentEvi == currentFrame.Source.Body.Length)
                    {
                        // if we reached the end and didn't meet Ret - it's a failboat
                        throw new UnexpectedElfRuntimeException(VM, String.Format(
                            "Thread has reached the end of the native method."));
                    }
                    else
                    {
                        var currentEvi = currentFrame.Source.Body[currentFrame.CurrentEvi];

                        // strictly saying, this isn't correct since void can be popped a bit later
                        // however, this check works fine with code generated by DefaultElfCompiler
                        if (!currentFrame.Stack.IsNullOrEmpty() &&
                            currentFrame.Stack.Peek() is ElfVoid && 
                            !(currentEvi is Pop || currentEvi is PopAll))
                        {
                            throw new ErroneousScriptRuntimeException(ElfExceptionType.UsingVoidValue, VM);
                        }

                        ProcessCurrentEvi(currentFrame, currentEvi);

                        if (Ctx.CallStack.Count == 0) Status = ThreadStatus.Finished;
                        return Ctx.CallStack.Count > 0;
                    }
                }
            }
            catch (TargetInvocationException tie)
            {
                Status = ThreadStatus.Crashed;
                if (tie.InnerException is ErroneousScriptRuntimeException) throw tie.InnerException;
                throw new UnexpectedRtimplRuntimeException(VM, String.Format(
                    "Fatal runtime error in an Elf thread. VM dump:{0}{1}",
                    Environment.NewLine, VM.DumpAll()), tie);
            }
            catch (Exception e)
            {
                Status = ThreadStatus.Crashed;
                if (e is ErroneousScriptRuntimeException) throw;
                if (e is UnexpectedRtimplRuntimeException) throw;
                throw new UnexpectedElfRuntimeException(VM, String.Format(
                    "Fatal runtime error in an Elf thread. VM dump:{0}{1}",
                    Environment.NewLine, VM.DumpAll()), e);
            }
            finally
            {
                VM.CurrentThread = old;
            }
        }

        private void ProcessCurrentEvi(NativeCallContext currentFrame, ElfVmInstruction currentEvi)
        {
            // perverted solution, but I really cba to introduce an enum for evis
            // neither I like solution with introduction of visitor
            var @this = Ctx.CallStack.Peek().Scopes.Last()["@this"];
            var scopeResolver = (IScopeResolver)Activator.CreateInstance(@this.Type.ScopeResolver);
            var invocationResolver = (IInvocationResolver)Activator.CreateInstance(@this.Type.InvocationResolver);

            switch (currentEvi.GetType().Name.ToLower())
            {
                case "decl":
                    var decl = (Decl)currentEvi;
                    scopeResolver.Declare(Ctx, decl.Name, @this);
                    ++currentFrame.CurrentEvi;
                    break;

                case "dup":
                    currentFrame.Stack.Push(currentFrame.Stack.Peek());
                    ++currentFrame.CurrentEvi;
                    break;

                case "enter":
                    scopeResolver.EnterScope(Ctx, @this);
                    ++currentFrame.CurrentEvi;
                    break;

                case "invoke":
                    var invoke = (Invoke)currentEvi;
                    var args = new IElfObject[invoke.Argc];
                    for (var i = invoke.Argc - 1; i >= 0; --i)
                        args[i] = currentFrame.Stack.Pop();

                    try {invocationResolver.PrepareCallContext(Ctx, invoke.Name, @this, args);}
                    catch (Exception e)
                    {
                        // that's pretty awful, tho logically correct
                        if (e is ErroneousScriptRuntimeException) throw;
                        throw new UnexpectedRtimplRuntimeException(VM, String.Format(
                            "Fatal runtime error in an Elf thread. VM dump:{0}{1}",
                            Environment.NewLine, VM.DumpAll()), e);
                    }

                    if (Ctx.PendingClrCall != null) ProcessClrCall(Ctx.PendingClrCall);
                    ++currentFrame.CurrentEvi;
                    break;

                case "jf":
                case "jt":
                    var test = currentFrame.Stack.Pop();
                    if (!(test is ElfBoolean))
                    {
                        throw new ErroneousScriptRuntimeException(ElfExceptionType.ConditionNotBoolean, VM);
                    }
                    else
                    {
                        var jf = currentEvi as Jf;
                        var jt = currentEvi as Jt;
                        var jlabel = jf != null ? jf.Label : jt.Label;

                        if ((((ElfBoolean)test).Val) ^ jf != null)
                        {
                            var target = currentFrame.Source.Body
                                .OfType<Label>().Single(b => b.Name == jlabel);
                            currentFrame.CurrentEvi =
                                Array.IndexOf(currentFrame.Source.Body, target);
                        }
                        else
                        {
                            ++currentFrame.CurrentEvi;
                        }
                    }
                    break;

                case "label":
                    ++currentFrame.CurrentEvi;
                    break;

                case "leave":
                    scopeResolver.LeaveScope(Ctx, @this);
                    ++currentFrame.CurrentEvi;
                    break;

                case "pop":
                    currentFrame.Stack.Pop();
                    ++currentFrame.CurrentEvi;
                    break;

                case "popall":
                    currentFrame.Stack.Clear();
                    ++currentFrame.CurrentEvi;
                    break;

                case "popref":
                    var value = currentFrame.Stack.Pop();
                    var popRef = (PopRef)currentEvi;

                    try {scopeResolver.Set(Ctx, popRef.Ref, value, @this);}
                    catch (Exception e)
                    {
                        // that's pretty awful, tho logically correct
                        if (e is ErroneousScriptRuntimeException) throw;
                        throw new UnexpectedRtimplRuntimeException(VM, String.Format(
                            "Fatal runtime error in an Elf thread. VM dump:{0}{1}",
                            Environment.NewLine, VM.DumpAll()), e);
                    }

                    ++currentFrame.CurrentEvi;
                    break;

                case "pushref":
                    var pushRef = (PushRef)currentEvi;

                    IElfObject value2;
                    try { value2 = scopeResolver.Get(Ctx, pushRef.Ref, @this); }
                    catch(Exception e)
                    {
                        // that's pretty awful, tho logically correct
                        if (e is ErroneousScriptRuntimeException) throw;
                        throw new UnexpectedRtimplRuntimeException(VM, String.Format(
                            "Fatal runtime error in an Elf thread. VM dump:{0}{1}",
                            Environment.NewLine, VM.DumpAll()), e);
                    }

                    currentFrame.Stack.Push(value2);
                    ++currentFrame.CurrentEvi;
                    break;

                case "pushval":
                    var pushVal = (PushVal)currentEvi;
                    var value3 = VM.Marshaller.Unmarshal(pushVal.Val);
                    currentFrame.Stack.Push(value3);
                    ++currentFrame.CurrentEvi;
                    break;

                case "ret":
                    var retval = currentFrame.Stack.SingleOrDefault() ?? new ElfVoid();
                    Ctx.CallStack.Pop();
                    if (Ctx.CallStack.Count > 0)
                    {
                        Ctx.CallStack.Peek().Stack.Push(retval);
                    }
                    else
                    {
                        ExecutionResult = retval;
                    }
                    break;

                default:
                    throw new UnexpectedElfRuntimeException(VM, String.Format(
                        "Fatal error executing evi '{0}'. Reason: evi type '{1}' not supported.",
                        currentEvi, currentEvi.GetType()));
            }
        }

        private void ProcessClrCall(ClrCallContext currentFrame)
        {
            var mb = currentFrame.Source.Rtimpl;

            var formal = mb.GetParameters().Select(pi => pi.ParameterType);
            var head = mb.IsStatic || mb.IsConstructor ? 0 : 1;
            var body = formal.Skip(head).Reverse().Skip(1).Reverse();
            var tailType = mb.IsVarargs() ? formal.Last().GetElementType() : formal.Last();
            var tailIterations = currentFrame.Args.Length - body.Count();
            var tail = Enumerable.Repeat(tailType, tailIterations);
            var sigArgs = body.Concat(tail).ToArray();

            var marshalledThis = mb.IsConstructor || mb.IsStatic ? null :
                mb.DeclaringType == currentFrame.This.GetType() ? currentFrame.This : VM.Marshaller.Marshal(currentFrame.This);
            var marshalledArgs = currentFrame.Args.Zip(sigArgs,
                (elf, pi) => typeof(IElfObject).IsAssignableFrom(pi) ? elf : VM.Marshaller.Marshal(elf)).ToArray();

            if (mb.IsVarargs())
            {
                var packedTail = Array.CreateInstance(tailType, tailIterations);
                marshalledArgs.Skip(body.Count()).ForEach(packedTail.SetValue);
                marshalledArgs = marshalledArgs.Take(body.Count()).Concat(packedTail.AsArray()).ToArray();
            }

            object retval;
            try
            {
                retval = mb.Invoke(marshalledThis, marshalledArgs);
            }
            catch(Exception ex)
            {
                if (ex is TargetInvocationException)
                {
                    throw;
                }
                else
                {
                    throw new ErroneousScriptRuntimeException(ElfExceptionType.OperandsDontSuitMethod, VM, ex);
                }
            }

            IElfObject retvalMarshalled;
            if (!mb.IsConstructor)
            {
                var mi = (MethodInfo)mb;
                retvalMarshalled = mi.ReturnType == typeof(void) ?
                    new ElfVoid() : VM.Marshaller.Unmarshal(retval);
            }
            else
            {
                retvalMarshalled = (IElfObject)retval;
            }

            Ctx.CallStack.Peek().Stack.Push(retvalMarshalled);
            Ctx.PendingClrCall = null;
        }

        public String Dump()
        {
            var sb = new StringBuilder();
            sb.AppendLine("Managed thread");
            sb.AppendFormat("Status = {0}", Status).AppendLine();
            sb.AppendLine("Entrypoint =");
            sb.AppendFormat("  {0}", EntryPoint.CodePoint).AppendLine();
            sb.AppendFormat("  This = {0}, Args = ({1})", EntryPoint.This, EntryPoint.Args.StringJoin()).AppendLine();
            sb.AppendLine().Append(Ctx.Dump());

            return sb.ToString();
        }
    }
}